package com.sec.android.app.myfiles.module.local.file;

import android.os.Bundle;
import android.os.FileObserver;
import android.os.Handler;
import android.os.Message;

import com.sec.android.app.myfiles.info.AppConstants;
import com.sec.android.app.myfiles.log.Log;
import com.sec.android.app.myfiles.module.abstraction.AbsContentObserverImp;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * <pre>
 * Created by daesu83.kim on 2015-07-21. 
 * this class observe the target folder. 
 * android.os.FileObserver class perform same thing, but it can't  observe same folder at the same time in multi-instance. 
 * Because it uses static variable to save target folder. 
 * this class has map, it saves mapping information of target folder with content change event listener.
 * </pre>
 */
public class LocalFileObserver {

    private static final int MASK = FileObserver.DELETE | FileObserver.DELETE_SELF | FileObserver.MOVE_SELF | FileObserver.MOVED_FROM
            | FileObserver.MOVED_TO | FileObserver.CLOSE_WRITE;

    private static LocalFileObserver sInstance = null;
    private HashMap<String, InternalFileObserver> mFileObserverMap;
    private HashMap<String, ArrayList<WeakReference<AbsContentObserverImp>>> mListenerListMap;
    private long mCumulativeTime = 0;
    private long mPrevTimeMillis = 0;

    private static final long INTERVAL = 300;
    private static final int FILE_CHANGED = 1;
    private static final int PENDING_FILE_CHANGED = 0;

    public static synchronized LocalFileObserver getInstance() {
        if (sInstance == null) {
            sInstance = new LocalFileObserver();
        }
        return sInstance;
    }

    private LocalFileObserver() {
        mFileObserverMap = new HashMap<>();
        mListenerListMap = new HashMap<>();
    }

    public synchronized void addPath(String path, AbsContentObserverImp listener) {
        InternalFileObserver observer = mFileObserverMap.get(path);
        if (observer == null) {
            observer = new InternalFileObserver(path, MASK);
            mFileObserverMap.put(path, observer);
            observer.startWatching();
        }

        ArrayList<WeakReference<AbsContentObserverImp>> listenerList = mListenerListMap.get(path);
        if (listenerList == null) {
            synchronized (observer) {
                listenerList = new ArrayList<>();
                mListenerListMap.put(path, listenerList);
            }
        }
        listenerList.add(new WeakReference<>(listener));
    }

    public synchronized void removePath(String path, AbsContentObserverImp listener) {
        InternalFileObserver observer = mFileObserverMap.get(path);
        synchronized (getListenerListLock(path)) {
            ArrayList<WeakReference<AbsContentObserverImp>> listenerList = mListenerListMap.get(path);
            if (listenerList != null) {
                AbsContentObserverImp observerImp;
                Iterator<WeakReference<AbsContentObserverImp>> it = listenerList.iterator();
                while (it.hasNext()) {
                    observerImp = it.next().get();
                    if (observerImp == null || listener.equals(observerImp)) {
                        it.remove();
                    }
                }

                if (listenerList.isEmpty()) {
                    if (observer != null) {
                        observer.stopWatching();
                    }
                    mListenerListMap.remove(path);
                    mFileObserverMap.remove(path);
                }
            }
        }
    }

    private synchronized Object getListenerListLock(String path) {
        InternalFileObserver observer = mFileObserverMap.get(path);
        Object lock;
        if (observer == null) {
            // dummy lock
            lock = new Object();
        } else {
            lock = observer;
        }
        return lock;
    }

    public class InternalFileObserver extends FileObserver {

        private String mObservePath;

        private Handler mHandler = new Handler(new Handler.Callback() {
            @Override
            public synchronized boolean handleMessage(Message msg) {
                ArrayList<WeakReference<AbsContentObserverImp>> listenerList = mListenerListMap.get(mObservePath);
                if (listenerList != null) {
                    AbsContentObserverImp listener;
                    synchronized (getListenerListLock(mObservePath)) {
                        for (WeakReference<AbsContentObserverImp> ref : listenerList) {
                            listener = ref.get();
                            if (listener != null) {
                                listener.onEvent(AppConstants.ObserverType.CURRENT_FOLDER);
                            }
                        }
                    }
                }
                return true;
            }
        });

        public InternalFileObserver(String path, int mask) {
            super(path, mask);
            mObservePath = path;
        }

        @Override
        public void onEvent(int event, String path) {
            long curTimeMillis = System.currentTimeMillis();

            int value = event & MASK;
            Log.d(this, "Mask : " + value);

            if (value > 0 && path != null) {
                mCumulativeTime += curTimeMillis - mPrevTimeMillis;
                mPrevTimeMillis = curTimeMillis;
                mHandler.removeMessages(PENDING_FILE_CHANGED);

                Message message = mHandler.obtainMessage();
                Bundle data = new Bundle();
                data.putInt("event", event);
                data.putString("path", path);
                message.setData(data);

                if (mCumulativeTime > INTERVAL) {
                    message.what = FILE_CHANGED;
                    mHandler.sendMessage(message);
                    mCumulativeTime = 0;
                } else {
                    message.what = PENDING_FILE_CHANGED;
                    mHandler.sendMessageDelayed(message, INTERVAL);
                }
            } else {
                Log.e(this, "OnEvent :  " + event + " is ignored (" + Log.getEncodedMsg(path) + ")");
            }
        }
    }
}
